package relu.fbs;

import java.util.*;

import jfm.lp.LPX;
import jfm.model.*;
import jfm.model.Types.CropType;
import jfm.model.Types.DiseaseType;
import relu.fbs.jfm.*;
import relu.fbs.jfm.AbstractJFMCropData.JFMCDatum;
import relu.fbs.jfm.JFMFarmData.JFMFarmDatum;

public final class ToJFM {
	/** Create a crops object containing all croptypes in the JFMFarmData 
	 * @param splitCrops is a map from an fbs croptype to multiple JFM crops required to represent it.
	 * @param base A base Farm object containing original Crop definitions read from an xml file 
	 * @param jfmd Data from the FBS for this farm 
	 * @param createAll If set to true all crops in the fbs will be created, even if their Area's are equal to zero */
	public static Map<CropType,Crop> generateCropList(Map<CropType,HashSet<CropType>> splitCrops,Farm base,JFMFarmData jfmd,boolean createAll){
		Map<CropType,Crop> baseCrops = base.cropping.copyCrops(); // Get deregistered copies of the base crops 
		Map<CropType,Crop> clist = new HashMap<CropType,Crop>();
		for ( Map.Entry<CropType, AbstractJFMCropData> entry:jfmd.crops().entrySet()){
			CropType ct=entry.getKey();
			JFMCropData cd = (JFMCropData)entry.getValue();
			if ( cd.getValue(JFMCDatum.AREA) > 0 || createAll ){
//				System.out.println(ct+" "+cd.getValue(JFMCDatum.AREA));
				if (!baseCrops.containsKey(ct)){
//					System.out.println("Key "+ct+" not in baseCrops");
					if (!splitCrops.containsKey(ct)){
						throw new Error("The crop cannot be generated because it is not present in the base model \n "
								+jfmd+" \n"+baseCrops.keySet()+"\n The offending crop is "+ct);
					} else {
//						System.out.println("building split "+ct+" "+splitCrops.get(ct));
						buildSplitCrop(splitCrops.get(ct),baseCrops,clist);
					}
				} else {
					clist.put(ct,baseCrops.get(ct));
				}
			}
		}
		// And don't forget setaside, which is not listed as a crop 
//		clist.put(CropType.SETASIDE,baseCrops.get(CropType.SETASIDE));
		return clist;
	}
	
	private static void buildSplitCrop(HashSet<CropType> splits,Map<CropType,Crop> baseCrops,Map<CropType,Crop> clist){
		// Create the sub crops required 
		for ( CropType ctsub:splits){
			if (!baseCrops.containsKey(ctsub)){
				throw new Error("The sub crop cannot be generated because it is not present in the base model \n "
						+baseCrops.keySet()+"\n The offending crop is "+ctsub);
			}
			if ( clist.containsKey(ctsub)){
				throw new Error("The sub crop "+ctsub+" already exists ");
			}
			clist.put(ctsub, baseCrops.get(ctsub));
		}
	}
	
	/** Set fbs yield data for a list of crop objects. 
	 * */
	public static void setCropDatum(Map<CropType,HashSet<CropType>> splitCrops,Map<CropType,Crop> clist,JFMFarmData jfmd,JFMCDatum dtIdentifier){
		for ( Map.Entry<CropType, AbstractJFMCropData> entry:jfmd.crops().entrySet()){
			CropType ct=entry.getKey();
			JFMCropData cd = (JFMCropData)entry.getValue();
			if (splitCrops.containsKey(ct) && clist.containsKey(ct) ){
				setSplitCropDatum(splitCrops.get(ct),clist,ct,cd,dtIdentifier);
			} else {
				if ( clist.containsKey(ct)){
					Crop cp = clist.get(ct);
					setCropDatum(cp,cd,dtIdentifier);
				}
			}			
		}
	}
	
	private static void setSplitCropDatum(HashSet<CropType> splitc,Map<CropType,Crop> clist,CropType ct,JFMCropData cd,JFMCDatum dtIdentifier){
		if ( dtIdentifier==JFMCDatum.AREA){
			return; // Area for split crops must be set with a limit constraint.
		}
		for(CropType ctsub:splitc){
			if ( !clist.containsKey(ctsub)){
				throw new Error("The croptype "+ctsub+" is undefined in the lp int split for "+ct+" \n"+clist.keySet());
			}
			Crop cp = clist.get(ctsub);
			setCropDatum(cp,cd,dtIdentifier);
		}
	}
	
	private static void setCropDatum(Crop cp,JFMCropData cd,JFMCDatum dtIdentifier){
		if ( cd.getValue(dtIdentifier) > 0 ){
			switch(dtIdentifier){
			case YIELD:
				Double yld=cd.getValue(JFMCDatum.YIELD);
				cp.resetPrimaryYield(yld.toString());
				break;
			case PRICE:
				Double pce=cd.getValue(JFMCDatum.PRICE);
				cp.resetPrimaryPrice(pce);
				break;
			case AREAPAYMENT:
				cp.resetSubsidy(cd.getValue(JFMCDatum.AREAPAYMENT));
				break;
			default:
				throw new Error("No set function defined for datum "+dtIdentifier);
			}
		} else {
			// FBS data does not exist for this quantity so leave it
			if ( cp.type != CropType.SETASIDE){
				System.out.println("Warning -- No Data for "+dtIdentifier+" on crop "+cp.type);
			} else {
				// Price for setaside is expected to be zero anyway so no warning needed
			}
		}
	}
	

	
	static Farm buildConstrained(Farm base,JFMFarmData jfm,double setAsideRate,boolean messagesOn) throws BadModelException {
		// Obtain a bare-bones version of the base farm to use as a template 
		Farm constrained = Farm.createTemplate(base);
		if(!messagesOn){
			constrained.disableSolverOutput();
		}
		
		// translate the overall farm Data 
		CroppingComponent cropping = constrained.cropping;
		double farmArea=jfm.getFarmDatum(JFMFarmDatum.MODELABLEAREA);
		cropping.setArea(farmArea);
		Map<CropType,Crop> crops = ToJFM.generateCropList(FarmData.jfmToLPMap,base, jfm, false);
		ToJFM.setCropDatum(FarmData.jfmToLPMap,crops, jfm, JFMCDatum.YIELD);
		ToJFM.setCropDatum(FarmData.jfmToLPMap,crops, jfm, JFMCDatum.PRICE);
		ToJFM.setCropDatum(FarmData.jfmToLPMap, crops, jfm,JFMCDatum.AREAPAYMENT);
	//	System.out.println("Adding crops to the cropping object ");
		for(Crop cp:crops.values()){
			cropping.addCrop(cp);
		}

		List<CropType> fixedAreaCrops=new ArrayList<CropType>();
		fixedAreaCrops.add(CropType.SUGARBEET);
		fixedAreaCrops.add(CropType.WAREPOTATOES);
	
		Set<CropType> setAsideCrops=getSetAsideCrops(constrained);
	//	System.out.println(setAsideCrops);
		// Put a lower limit on set-aside 
		double minSetAsideArea=farmArea*setAsideRate;
		buildLimitConstraints(constrained.cropping,fixedAreaCrops,jfm);
		Limit setAsideLim=Limit.CropAreaLimit(setAsideCrops, LPX.LPX_LO, minSetAsideArea, 0.0);
		cropping.addLimit(setAsideLim);
		
		// And an upper limit to crops grown on setaside 10 pc .. should not be needed.
		setAsideCrops.remove(CropType.SETASIDE);
		Limit setAsideUpLim=Limit.CropAreaLimit(setAsideCrops, LPX.LPX_UP, 0.0 , minSetAsideArea);
		cropping.addLimit(setAsideUpLim);
		setAsideCrops.add(CropType.SETASIDE);
		
		if (!cropping.diseasesAndRotationsComplete()){
			throw new Error("Some more diseases and/or rotations for the specified crops");
		}
		
		return constrained;
	}
	
	static Set<CropType> getSetAsideCrops(Farm farm){
		Set<CropType> setAsideCrops=new HashSet<CropType>();
		setAsideCrops.add(CropType.SETASIDE);
		for ( CropType sact:FarmData.jfmSetAsideCrops.values()){
			if ( farm.cropping.baseCropTypes().contains(sact)){
				setAsideCrops.add(sact);
			}
		}
		return setAsideCrops;
	}
	
	static void buildLimitConstraints(Cropping cropping,List<CropType> fixCrops,JFMFarmData jfm){
		for(CropType ct:fixCrops){
			if ( cropping.baseCropTypes().contains(ct)){
				double area=jfm.crops().get(ct).getValue(JFMCDatum.AREA);
				Limit lim = Limit.CropAreaLimit(ct,LPX.LPX_FX,area,area);
				cropping.addLimit(lim);
				/* Since we have placed a limit on the area of this crop we should partly relax the corresponding disease Constraint 
				 * The way this is done here will only work if each disease is associated with just one crop */
				DiseaseType disType = cropping.getCrop(ct).diseaseType;
				Disease dis=cropping.getDisease(disType);
//				System.out.println("Limiting area for "+ct+" to "+area+" with disease "+disType);
				cropping.replaceDisease(disType, new Disease(dis.base,dis.associated,0));
			}
		}
	}
	

	static double totalSetAside(Farm farm){
		Set<CropType> setAsideCrops=getSetAsideCrops(farm);
		double total=0;
		for(CropType ct:setAsideCrops){
			total+=farm.cropping.getCrop(ct).getSolvedArea();
		}
		return total;
	}

	
}
